//
//  Ch3_6Controller.swift
//  learn-gl-es-swift
//
//  Created by LimitLiu on 2019/9/11.
//  Copyright © 2019 LimitLiu. All rights reserved.
//

import UIKit
import GLKit

struct SceneVertex3_6 {
    var position: GLKVector3
    var normal: GLKVector3
    var texture: GLKVector2
    init(_ p: GLKVector3, _ n: GLKVector3, _ t: GLKVector2) {
        position = p
        normal = n
        texture = t
    }
}

enum Uniform: GLint {
    case modelViewProjectionMatrix
    case normalMatrix
    case texture0Sampler2D
    case texture1Sampler2D
    case numberOfUniforms
}

class Ch3_6Controller: GLKViewController {
    
    var baseEffect = GLKBaseEffect()
    var vertexBuffer: GLVertexAttribArrayBuffer!
    var program: GLuint = GLuint()
    var uniforms: [GLint] = [GLint](repeating: 0, count: Int(Uniform.numberOfUniforms.rawValue))
    var normalMatrix: GLKMatrix3?
    var modelViewProjectionMatrix: GLKMatrix4?
    var rotation: GLfloat = GLfloat()

    var vertices:[SceneVertex3_6] = [
        SceneVertex3_6(vec( 0.5, -0.5, -0.5), vec( 1.0,  0.0,  0.0), vec(0.0, 0.0)),
        SceneVertex3_6(vec( 0.5,  0.5, -0.5), vec( 1.0,  0.0,  0.0), vec(1.0, 0.0)),
        SceneVertex3_6(vec( 0.5, -0.5,  0.5), vec( 1.0,  0.0,  0.0), vec(0.0, 1.0)),
        SceneVertex3_6(vec( 0.5, -0.5,  0.5), vec( 1.0,  0.0,  0.0), vec(0.0, 1.0)),
        SceneVertex3_6(vec( 0.5,  0.5,  0.5), vec( 1.0,  0.0,  0.0), vec(1.0, 1.0)),
        SceneVertex3_6(vec( 0.5,  0.5, -0.5), vec( 1.0,  0.0,  0.0), vec(1.0, 0.0)),
        
        SceneVertex3_6(vec( 0.5,  0.5, -0.5), vec( 0.0,  1.0,  0.0), vec(1.0, 0.0)),
        SceneVertex3_6(vec(-0.5,  0.5, -0.5), vec( 0.0,  1.0,  0.0), vec(0.0, 0.0)),
        SceneVertex3_6(vec( 0.5,  0.5,  0.5), vec( 0.0,  1.0,  0.0), vec(1.0, 1.0)),
        SceneVertex3_6(vec( 0.5,  0.5,  0.5), vec( 0.0,  1.0,  0.0), vec(1.0, 1.0)),
        SceneVertex3_6(vec(-0.5,  0.5, -0.5), vec( 0.0,  1.0,  0.0), vec(0.0, 0.0)),
        SceneVertex3_6(vec(-0.5,  0.5,  0.5), vec( 0.0,  1.0,  0.0), vec(0.0, 1.0)),
        
        SceneVertex3_6(vec(-0.5,  0.5, -0.5), vec(-1.0,  0.0,  0.0), vec(1.0, 0.0)),
        SceneVertex3_6(vec(-0.5, -0.5, -0.5), vec(-1.0,  0.0,  0.0), vec(0.0, 0.0)),
        SceneVertex3_6(vec(-0.5,  0.5,  0.5), vec(-1.0,  0.0,  0.0), vec(1.0, 1.0)),
        SceneVertex3_6(vec(-0.5,  0.5,  0.5), vec(-1.0,  0.0,  0.0), vec(1.0, 1.0)),
        SceneVertex3_6(vec(-0.5, -0.5, -0.5), vec(-1.0,  0.0,  0.0), vec(0.0, 0.0)),
        SceneVertex3_6(vec(-0.5, -0.5,  0.5), vec(-1.0,  0.0,  0.0), vec(0.0, 1.0)),
        
        SceneVertex3_6(vec(-0.5, -0.5, -0.5), vec( 0.0, -1.0,  0.0), vec(0.0, 0.0)),
        SceneVertex3_6(vec( 0.5, -0.5, -0.5), vec( 0.0, -1.0,  0.0), vec(1.0, 0.0)),
        SceneVertex3_6(vec(-0.5, -0.5,  0.5), vec( 0.0, -1.0,  0.0), vec(0.0, 1.0)),
        SceneVertex3_6(vec(-0.5, -0.5,  0.5), vec( 0.0, -1.0,  0.0), vec(0.0, 1.0)),
        SceneVertex3_6(vec( 0.5, -0.5, -0.5), vec( 0.0, -1.0,  0.0), vec(1.0, 0.0)),
        SceneVertex3_6(vec( 0.5, -0.5,  0.5), vec( 0.0, -1.0,  0.0), vec(1.0, 1.0)),
        
        SceneVertex3_6(vec( 0.5,  0.5,  0.5), vec( 0.0,  0.0,  1.0), vec(1.0, 1.0)),
        SceneVertex3_6(vec(-0.5,  0.5,  0.5), vec( 0.0,  0.0,  1.0), vec(0.0, 1.0)),
        SceneVertex3_6(vec( 0.5, -0.5,  0.5), vec( 0.0,  0.0,  1.0), vec(1.0, 0.0)),
        SceneVertex3_6(vec( 0.5, -0.5,  0.5), vec( 0.0,  0.0,  1.0), vec(1.0, 0.0)),
        SceneVertex3_6(vec(-0.5,  0.5,  0.5), vec( 0.0,  0.0,  1.0), vec(0.0, 1.0)),
        SceneVertex3_6(vec(-0.5, -0.5,  0.5), vec( 0.0,  0.0,  1.0), vec(0.0, 0.0)),
        
        SceneVertex3_6(vec( 0.5, -0.5, -0.5), vec( 0.0,  0.0, -1.0), vec(4.0, 0.0)),
        SceneVertex3_6(vec(-0.5, -0.5, -0.5), vec( 0.0,  0.0, -1.0), vec(0.0, 0.0)),
        SceneVertex3_6(vec( 0.5,  0.5, -0.5), vec( 0.0,  0.0, -1.0), vec(4.0, 4.0)),
        SceneVertex3_6(vec( 0.5,  0.5, -0.5), vec( 0.0,  0.0, -1.0), vec(4.0, 4.0)),
        SceneVertex3_6(vec(-0.5, -0.5, -0.5), vec( 0.0,  0.0, -1.0), vec(0.0, 0.0)),
        SceneVertex3_6(vec(-0.5,  0.5, -0.5), vec( 0.0,  0.0, -1.0), vec(0.0, 4.0)),
    ]
    deinit {
        let view = self.view as! GLKView
        GLContext.setCurrent(view.context)
        vertexBuffer = nil
        if program != 0 {
            glDeleteProgram(program)
            program = GLuint()
        }
        GLContext.setCurrent(nil)
    }
    
    override var shouldAutorotate: Bool {
        if UIDevice.current.userInterfaceIdiom == .phone {
            return !UIDevice.current.orientation.isPortrait
        }
        return true
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.delegate = self
        let view = self.view as! GLKView
        
        view.context = GLContext(api: .openGLES2)!
        view.drawableDepthFormat = .format24
        GLContext.setCurrent(view.context)
        
        if !loadShaders() {
            fatalError("failed to load shaders")
        }
        
        baseEffect.light0.enabled = GLboolean(GL_TRUE)
        baseEffect.light0.diffuseColor = vec(0.7, 0.7, 0.7, 1)
        glEnable(GLenum(GL_DEPTH_TEST))
        
        (view.context as! GLContext).clearColor = vec(0.65, 0.65, 0.65, 1)
        vertexBuffer = GLVertexAttribArrayBuffer(
            attribStride: GLsizei(MemoryLayout<SceneVertex3_6>.stride),
            numberOfVertices: GLsizei(vertices.count),
            data: vertices,
            usage: GLenum(GL_STATIC_DRAW)
        )
        let imageRef0 = UIImage(named: "leaves")!.cgImage!
        let textureInfo0 = try! GLKTextureLoader.texture(
            with: imageRef0,
            options: [GLKTextureLoaderOriginBottomLeft : true as NSNumber]
        )
        baseEffect.texture2d0.name = textureInfo0.name
        baseEffect.texture2d0.target = textureInfo0.getTarget
        baseEffect.texture2d0.set(parameterId: GLenum(GL_TEXTURE_WRAP_S), value: GLint(GL_REPEAT))
        baseEffect.texture2d0.set(parameterId: GLenum(GL_TEXTURE_WRAP_T), value: GLint(GL_REPEAT))
        
        let imageRef1 = UIImage(named: "leaves")!.cgImage!
        let textureInfo1 = try! GLKTextureLoader.texture(with: imageRef1, options: [GLKTextureLoaderOriginBottomLeft : true as NSNumber])
        baseEffect.texture2d1.name = textureInfo1.name
        baseEffect.texture2d1.target = textureInfo1.getTarget
        baseEffect.texture2d1.envMode = GLKTextureEnvMode.decal
        baseEffect.texture2d1.set(parameterId: GLenum(GL_TEXTURE_WRAP_S), value: GLint(GL_REPEAT))
        baseEffect.texture2d1.set(parameterId: GLenum(GL_TEXTURE_WRAP_T), value: GLint(GL_REPEAT))
    }
    
    override func glkView(_ view: GLKView, drawIn rect: CGRect) {
        (view.context as! GLContext).clear(GLbitfield(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT))
        vertexBuffer.prepareToDraw(
            attrib: GLuint(GLKVertexAttrib.position.rawValue),
            numberOfCoordinates: 3,
            attribOffset: GLsizeiptr(0),
            shouldEnable: true
        )
        
        vertexBuffer.prepareToDraw(
            attrib: GLuint(GLKVertexAttrib.normal.rawValue),
            numberOfCoordinates: 3,
            attribOffset: GLsizeiptr(MemoryLayout<GLfloat>.size * 3),
            shouldEnable: true
        )
        
        vertexBuffer.prepareToDraw(
            attrib: GLuint(GLKVertexAttrib.texCoord0.rawValue),
            numberOfCoordinates: 2,
            attribOffset: GLsizeiptr(MemoryLayout<GLfloat>.size * 6),
            shouldEnable: true
        )
        
        vertexBuffer.prepareToDraw(
            attrib: GLuint(GLKVertexAttrib.texCoord1.rawValue),
            numberOfCoordinates: 2,
            attribOffset: GLsizeiptr(MemoryLayout<GLfloat>.size * 6),
            shouldEnable: true
        )
        baseEffect.prepareToDraw()
        vertexBuffer.drawArray(
            mode: GLenum(GL_TRIANGLES),
            startVertexIdx: 0,
            numberOfVertices: GLsizei(vertices.count)
        )
        
        glUseProgram(program)
        
        if modelViewProjectionMatrix != nil && normalMatrix != nil {
            glUniformMatrix4fv(uniforms[Int(Uniform.modelViewProjectionMatrix.rawValue)], 1, 0, modelViewProjectionMatrix!.array)
            glUniformMatrix3fv(
                uniforms[Int(Uniform.normalMatrix.rawValue)],
                1, 0,
                normalMatrix!.array
            )
            glUniform1i(uniforms[Int(Uniform.texture0Sampler2D.rawValue)], 0)
            glUniform1i(uniforms[Int(Uniform.texture1Sampler2D.rawValue)], 1)
            glDrawArrays(GLenum(GL_TRIANGLES), 0, GLsizei(vertices.count))
        }
    }
}

extension Ch3_6Controller {
    func compile(shader: inout GLuint, type: GLenum, file: String) -> Bool {
        guard var src = ((try? NSString(contentsOfFile: file, encoding: String.Encoding.utf8.rawValue).utf8String) as UnsafePointer<Int8>??) else {
            print("Failed to load vertex shader")
            return false
        }
        shader = glCreateShader(type)
        glShaderSource(shader, 1, &src, nil)
        glCompileShader(shader)
        
        var len = GLint()
        glGetProgramiv(program, GLenum(GL_INFO_LOG_LENGTH), &len)
        if len > 0 {
            var l = [GLchar](repeating: 0, count: Int(len))
            glGetProgramInfoLog(program, len, &len, &l)
            print("shader compile log:\(l)")
        }
        var status = GLint()
        glGetShaderiv(shader, GLenum(GL_COMPILE_STATUS), &status)
        return status == GL_TRUE
    }
    
    func link(_ program: GLuint) -> Bool {
        glLinkProgram(program)
        var len = GLint()
        glGetProgramiv(program, GLenum(GL_INFO_LOG_LENGTH), &len)
        if len > 0 {
            var l = [GLchar](repeating: 0, count: Int(len))
            glGetProgramInfoLog(program, len, &len, &l)
            print("Program link log:\(l)")
        }
        var status = GLint()
        glGetProgramiv(program, GLenum(GL_LINK_STATUS), &status)
        return status == GL_TRUE
    }
    
    func validate(_ program: GLuint) -> Bool {
        var len = GLint()
        var status = Int32()
        glValidateProgram(program)
        glGetProgramiv(program, GLenum(GL_INFO_LOG_LENGTH), &len)
        if len > 0 {
            var log = [GLchar](repeating: 0, count: Int(len))
            glGetProgramInfoLog(program, len, &len, &log)
            print("Program validate log:\(log)")
        }
        glGetProgramiv(program, GLenum(GL_VALIDATE_STATUS), &status)
        return status == GL_TRUE
    }
    
    func loadShaders() -> Bool {
        program = glCreateProgram()
        var vertex = GLuint()
        let vertexPath = Bundle.main.path(forResource: "shader", ofType: "vert")!
        if !compile(shader: &vertex, type: GLenum(GL_VERTEX_SHADER), file: vertexPath) {
            print("Failed to compile vertex shader")
            return false
        }
        var fragment = GLuint()
        let fragmentPath = Bundle.main.path(forResource: "shader", ofType: "frag")!
        if !compile(shader: &fragment, type: GLenum(GL_FRAGMENT_SHADER), file: fragmentPath) {
            print("Failed to compile fragment shader")
            return false
        }
        glAttachShader(program, vertex)
        glAttachShader(program, fragment)
        
        glBindAttribLocation(program, GLuint(GLKVertexAttrib.position.rawValue), "aPosition")
        glBindAttribLocation(program, GLuint(GLKVertexAttrib.normal.rawValue), "aNormal")
        glBindAttribLocation(program, GLuint(GLKVertexAttrib.texCoord0.rawValue), "aTextureCoord0")
        glBindAttribLocation(program, GLuint(GLKVertexAttrib.texCoord1.rawValue), "aTextureCoord1")
        
        if !link(program) {
            print("Failed to link program: \(program)")
            if vertex != 0 {
                glDeleteShader(vertex)
                vertex = GLuint()
            }
            if fragment != 0 {
                glDeleteShader(fragment)
                fragment = GLuint()
            }
            if program != 0 {
                glDeleteProgram(program)
                program = GLuint()
            }
            return false
        }
        uniforms[Int(Uniform.modelViewProjectionMatrix.rawValue)] = glGetUniformLocation(
            program, "uModelViewProjectionMatrix"
        )
        
        uniforms[Int(Uniform.normalMatrix.rawValue)] = glGetUniformLocation(
            program, "uNormalMatrix"
        )
        
        uniforms[Int(Uniform.texture0Sampler2D.rawValue)] = glGetUniformLocation(
            program, "uSampler0"
        )
        
        uniforms[Int(Uniform.texture1Sampler2D.rawValue)] = glGetUniformLocation(
            program, "uSampler1"
        )
        
        if vertex != 0 {
            glDeleteShader(vertex)
            vertex = GLuint()
        }
        if fragment != 0 {
            glDeleteShader(fragment)
            fragment = GLuint()
        }
        
        return true
    }
}

extension Ch3_6Controller: GLKViewControllerDelegate {
    func glkViewControllerUpdate(_ controller: GLKViewController) {
        let aspect = fabsf(Float(view.bounds.size.width) / Float(view.bounds.size.height))
        let projectionMatrix = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(65), aspect, 0.1, 100)
        baseEffect.transform.projectionMatrix = projectionMatrix
        var baseModeViewMatrix = GLKMatrix4MakeTranslation(0, 0, -4)
        baseModeViewMatrix = GLKMatrix4Rotate(baseModeViewMatrix, rotation, 0, 1, 0)
        var modelViewMatrix = GLKMatrix4MakeTranslation(0, 0, -1.5)
        modelViewMatrix = GLKMatrix4Rotate(modelViewMatrix, rotation, 1, 1, 1)
        modelViewMatrix = GLKMatrix4Multiply(baseModeViewMatrix, modelViewMatrix)
        
        baseEffect.transform.modelviewMatrix = modelViewMatrix
        
        modelViewMatrix = GLKMatrix4MakeTranslation(0, 0, 1.5)
        modelViewMatrix = GLKMatrix4Rotate(modelViewMatrix, rotation, 1, 1, 1)
        modelViewMatrix = GLKMatrix4Multiply(baseModeViewMatrix, modelViewMatrix)
        
        normalMatrix = GLKMatrix4GetMatrix3(GLKMatrix4InvertAndTranspose(modelViewMatrix, nil))
        modelViewProjectionMatrix = GLKMatrix4Multiply(projectionMatrix, modelViewMatrix)
        
        rotation += GLfloat(timeSinceLastUpdate * 0.5)
    }
}
